package com.cc.utils;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.util.StopWatch;

import java.io.*;
import java.nio.charset.Charset;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

/**
 * 文件描述 zip压缩工具
 *
 * @author
 * @date
 */
@Slf4j(topic = "zip压缩")
public class ZipUtils {

  /**
   * 缓冲大小
   */
  private static final int BUFFER_SIZE = 2 * 1024;

  /**
   * 压缩成ZIP 方法1
   *
   * @param srcDir           压缩文件夹路径
   * @param out              压缩文件输出流
   * @param keepDirStructure 是否保留原来的目录结构,true:保留目录结构;
   *                         false:所有文件跑到压缩包根目录下
   *                         (注意：不保留目录结构可能会出现同名文件,会压缩失败)
   * @throws RuntimeException 压缩失败会抛出运行时异常
   */
  public static void toZip(String srcDir, OutputStream out, boolean keepDirStructure) {
    StopWatch stopWatch = new StopWatch();
    stopWatch.start("toZip-1");
    ZipOutputStream zos = null;
    try {
      zos = new ZipOutputStream(out);
      File sourceFile = new File(srcDir);
      compress(sourceFile, zos, sourceFile.getName(), keepDirStructure);
    } catch (Exception e) {
      log.error("压缩zip出错,", e);
      throw new RuntimeException("压缩zip出错");
    } finally {
      close(zos);
    }
    stopWatch.stop();
    log.info("toZip-1 共耗时：{}", stopWatch.getTotalTimeMillis());
  }

  /**
   * 压缩成ZIP 方法2
   *
   * @param srcFiles 需要压缩的文件列表
   * @param out      压缩文件输出流
   * @throws RuntimeException 压缩失败会抛出运行时异常
   */
  public static void toZip(List<File> srcFiles, OutputStream out) throws RuntimeException {
    StopWatch stopWatch = new StopWatch();
    stopWatch.start("toZip-2");
    ZipOutputStream zos = null;
    try {
      zos = new ZipOutputStream(out);
      for (File sourceFile : srcFiles) {
        byte[] buf = new byte[BUFFER_SIZE];
        zos.putNextEntry(new ZipEntry(sourceFile.getName()));
        int len;
        FileInputStream in = new FileInputStream(sourceFile);
        while ((len = in.read(buf)) != -1) {
          zos.write(buf, 0, len);
        }
        zos.closeEntry();
        in.close();
      }
    } catch (Exception e) {
      log.error("压缩zip出错,", e);
      throw new RuntimeException("压缩zip出错");
    } finally {
      close(zos);
    }
    stopWatch.stop();
    log.info("toZip-2 共耗时：{}", stopWatch.getTotalTimeMillis());
  }

  /**
   * 压缩多个文件成一个zip文件
   *
   * @param srcFiles：源文件列表
   * @param destZipFile：压缩后的文件
   */
  public static void toZip(File[] srcFiles, File destZipFile) {
    byte[] buf = new byte[1024];
    try {
      // ZipOutputStream类：完成文件或文件夹的压缩
      ZipOutputStream out = new ZipOutputStream(new FileOutputStream(destZipFile));
      for (int i = 0; i < srcFiles.length; i++) {
        FileInputStream in = new FileInputStream(srcFiles[i]);
        // 给列表中的文件单独命名
        out.putNextEntry(new ZipEntry(srcFiles[i].getName()));
        int len;
        while ((len = in.read(buf)) > 0) {
          out.write(buf, 0, len);
        }
        out.closeEntry();
        in.close();
      }
      out.close();
      log.info("压缩完成.");
    } catch (Exception e) {
      log.error("压缩出错,", e);
    }
  }

  /**
   * 关闭流
   *
   * @param os os
   * @return void
   * @date 2020 /6/2 13:33
   * @author fujw27320
   * @modifier
   */
  public static void close(OutputStream os) {
    if (null != os) {
      try {
        os.close();
      } catch (IOException e) {
        log.error("关闭流出错,", e);
        throw new RuntimeException("关闭流异常");
      }
    }
  }

  /**
   * 递归压缩方法
   *
   * @param sourceFile       源文件
   * @param zos              zip输出流
   * @param name             压缩后的名称
   * @param keepDirStructure 是否保留原来的目录结构,true:保留目录结构;                         false:所有文件跑到压缩包根目录下
   *                         (注意：不保留目录结构可能会出现同名文件,会压缩失败)
   * @throws Exception exception
   */
  private static void compress(File sourceFile, ZipOutputStream zos, String name, boolean keepDirStructure)
    throws Exception {
    byte[] buf = new byte[BUFFER_SIZE];
    if (sourceFile.isFile()) {
      // 向zip输出流中添加一个zip实体，构造器中name为zip实体的文件的名字
      zos.putNextEntry(new ZipEntry(name));
      // copy文件到zip输出流中
      int len;
      FileInputStream in = new FileInputStream(sourceFile);
      while ((len = in.read(buf)) != -1) {
        zos.write(buf, 0, len);
      }
      // Complete the entry
      zos.closeEntry();
      in.close();
    } else {
      File[] listFiles = sourceFile.listFiles();
      if (ArrayUtils.isEmpty(listFiles)) {
        // 需要保留原来的文件结构时,需要对空文件夹进行处理
        if (keepDirStructure) {
          // 空文件夹的处理
          zos.putNextEntry(new ZipEntry(name + "/"));
          // 没有文件，不需要文件的copy
          zos.closeEntry();
        }
      } else {
        for (File file : listFiles) {
          // 判断是否需要保留原来的文件结构
          if (keepDirStructure) {
            // 注意：file.getName()前面需要带上父文件夹的名字加一斜杠,
            // 不然最后压缩包中就不能保留原来的文件结构,即：所有文件都跑到压缩包根目录下了
            compress(file, zos, name + "/" + file.getName(), keepDirStructure);
          } else {
            compress(file, zos, file.getName(), keepDirStructure);
          }
        }
      }
    }
  }

  /**
   * 解压文件
   *
   * @param zipFile：需要解压缩的文件
   * @param descDir：解压后的目标目录
   */
  public static void unZipFiles(File zipFile, String descDir) throws IOException {
    File destFile = new File(descDir);
    if (!destFile.exists()) {
      destFile.mkdirs();
    }
    // 解决zip文件中有中文目录或者中文文件
    ZipFile zip = new ZipFile(zipFile, Charset.forName("GBK"));
    for (Enumeration entries = zip.entries(); entries.hasMoreElements(); ) {
      ZipEntry entry = (ZipEntry) entries.nextElement();
      InputStream in = zip.getInputStream(entry);
      String curEntryName = entry.getName();
      // 判断文件名路径是否存在文件夹
      int endIndex = curEntryName.lastIndexOf('/');
      // 替换
      String outPath = (descDir + curEntryName).replaceAll("\\*", "/");
      if (endIndex != -1) {
        File file = new File(outPath.substring(0, outPath.lastIndexOf("/")));
        if (!file.exists()) {
          file.mkdirs();
        }
      }

      // 判断文件全路径是否为文件夹,如果是上面已经上传,不需要解压
      File outFile = new File(outPath);
      if (outFile.isDirectory()) {
        continue;
      }
      OutputStream out = new FileOutputStream(outPath);
      byte[] buf1 = new byte[1024];
      int len;
      while ((len = in.read(buf1)) > 0) {
        out.write(buf1, 0, len);
      }
      in.close();
      out.close();
    }
    log.info("解压{}成功", zipFile.getName());
  }

}
